﻿using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Text;

using Cookbook.Common;

using Microsoft.Practices.EnterpriseLibrary.Common.Configuration;
using Microsoft.Practices.EnterpriseLibrary.Logging;
using Microsoft.Practices.EnterpriseLibrary.Logging.Configuration;
using Microsoft.Practices.EnterpriseLibrary.Logging.Formatters;

namespace Cookbook.Services.Core.Common
{
    /// <summary>
    /// Provides similar text formatting services as the Enterprise Library <see cref="TextFormatter"/>, but 
    /// adds additional formatting options.
    /// </summary>
    /// <remarks>
    /// <list type="bullet">
    /// <item><see cref="string.Format(string,object)"/> specifiers for setting the layout. E.g. {tag, -1} or {tag, 20}</item>
    /// <item>The date-time format can be customized with the <i>dateTimeFormat</i> attribute</item>
    /// <item>A <i>user</i> tag for displaying the current identity name</item>
    /// </list>
    /// </remarks>
    [ConfigurationElementType(typeof (CustomFormatterData))]
    public class ExtendedFormatter : ILogFormatter
    {
        private readonly string dateTimeFormat = "yyyy-MM-dd hh:mm:ss.ff";
        private readonly string template = "";

        /// <summary>
        /// Initializes a new instance of the <see cref="ExtendedFormatter"/> class.
        /// </summary>
        /// <param name="attributes">The attributes.</param>
        public ExtendedFormatter(NameValueCollection attributes)
        {
            template = attributes["template"];
            if (template.IsNullOrEmpty())
            {
                throw new LoggingException("template is a required attribute");
            }

            if (attributes["dateTimeFormat"] != null)
            {
                dateTimeFormat = attributes["dateTimeFormat"];
            }
        }

        ///<summary>
        ///
        ///            Formats a log entry and return a string to be outputted.
        ///            
        ///</summary>
        ///
        ///<param name="log">Log entry to format.</param>
        ///<returns>
        ///String representing the log entry.
        ///</returns>
        ///
        public string Format(LogEntry log)
        {
            var resultBuilder = new StringBuilder();

            // Split the message, so every line of the message is prefixed is templated
            // Not using the Environment.Newline to accept input that returns only \n instead of \r\n
            string[] messageParts = log.Message.Replace("\r", "").Split('\n');

            for (int index = 0; index < messageParts.Length; index++)
            {
                TemplateParser parser = BuildParserFromLogEntry(log);

                parser.ReplaceTag("message", messageParts[index]);

                resultBuilder.Append(parser.ToString());

                if (index != (messageParts.Length - 1))
                {
                    resultBuilder.Append(Environment.NewLine);
                }
            }

            return resultBuilder.ToString();
        }

        private TemplateParser BuildParserFromLogEntry(LogEntry log)
        {
            var builder = new TemplateParser(template);

            builder.ReplaceTag("timestamp", log.TimeStamp.ToLocalTime().ToString(dateTimeFormat));
            builder.ReplaceTag("title", log.Title);
            builder.ReplaceTag("eventid", log.EventId);
            builder.ReplaceTag("priority", log.Priority);
            builder.ReplaceTag("severity", log.Severity);
            builder.ReplaceTag("machine", log.MachineName);
            builder.ReplaceTag("appDomain", log.AppDomainName);
            builder.ReplaceTag("processId", log.ProcessId);
            builder.ReplaceTag("processName", log.ProcessName);
            builder.ReplaceTag("win32ThreadId", log.Win32ThreadId);
            builder.ReplaceTag("threadName", log.ManagedThreadName);
            builder.ReplaceTag("categories", string.Join(" ", log.CategoriesStrings));

            if (log.ExtendedProperties.ContainsKey("sessionId"))
            {
                builder.ReplaceTag("sessionId", log.ExtendedProperties["sessionId"]);
            }

            if (log.ExtendedProperties.ContainsKey("applicationName"))
            {
                builder.ReplaceTag("applicationName", log.ExtendedProperties["applicationName"]);
            }

            if (log.ExtendedProperties.ContainsKey("user"))
            {
                builder.ReplaceTag("user", log.ExtendedProperties["user"]);
            }
            
            return builder;
        }

        /// <summary>
        /// Helper class that ensures that the placeholders are properly used during replacement.
        /// </summary>
        internal class TemplateParser
        {
            private readonly StringBuilder templateBuilder;
            private readonly List<object> values = new List<object>();
            private int placeHolderIndex;

            /// <summary>
            /// Initializes a new instance of the <see cref="TemplateParser"/> class.
            /// </summary>
            /// <param name="template">The template.</param>
            public TemplateParser(string template)
            {
                templateBuilder = new StringBuilder(template);
                templateBuilder.Replace("{newline}", Environment.NewLine);
                templateBuilder.Replace("{tab}", "\t");
            }

            /// <summary>
            /// Replaces the specified {tag} with a value, while preserving the formatting. 
            /// </summary>
            public void ReplaceTag(string tag, object value)
            {
                templateBuilder.Replace("{" + tag, "{" + placeHolderIndex);

                var textValue = value as string;
                if (textValue != null)
                {
                    value = textValue.Replace("{", "{{").Replace("}", "}}");
                }

                values.Add(value);
                ++placeHolderIndex;
            }

            ///<summary>
            ///Returns a <see cref="T:System.String"></see> that represents the current <see cref="T:System.Object"></see>.
            ///</summary>
            ///
            ///<returns>
            ///A <see cref="T:System.String"></see> that represents the current <see cref="T:System.Object"></see>.
            ///</returns>
            ///<filterpriority>2</filterpriority>
            public override string ToString()
            {
                try
                {
                    return string.Format(templateBuilder.ToString(), values.ToArray());
                }
                catch (FormatException)
                {
                    throw new LoggingException("The template contains unknown tags or unbalanced parentheses");
                }
            }
        }
    }
}